Source: ICO Coffee Dataset
The selected dataset contains coffee stats worldwide by 13 different features from 1990 to 2018.
In the project, the following files from this dataset were used for visualizations:
Also, one additional file (world-countries.json) with polygon data was used for one of the visualizations.
import altair as alt
import pandas as pd
import geopandas as gpd
The visualization aim is to show how the imported coffee amount in each country has changed over the years.
The best solution for this visualization, in my opinion, is to display the data on a map using a geopandas library, encoding the amount of exported coffee in color. The color pallet is 'goldgreen': yellow displays lower values; the higher the green hue, the higher the value.
Tooltips with country, import, and year information are also added.
The map is interactive: a user can filter the data by years using a slider under the map.
The main advantage of the visualization is the convenience of viewing the whole picture in the world's changing periods.
Disadvantages: a relatively large numbers range (0 ~ 30,000) divided into a relatively small number of color ranges (step - 4000) -> Impossibility to compare values from the same interval without information from tooltips.
world = gpd.read_file('data/world-countries.json')
world.columns = ['id', 'Country', 'geometry']
world.head()
| id | Country | geometry | |
|---|---|---|---|
| 0 | AFG | Afghanistan | POLYGON ((61.21082 35.65007, 62.23065 35.27066... |
| 1 | AGO | Angola | MULTIPOLYGON (((16.32653 -5.87747, 16.57318 -6... |
| 2 | ALB | Albania | POLYGON ((20.59025 41.85540, 20.46317 41.51509... |
| 3 | ARE | United Arab Emirates | POLYGON ((51.57952 24.24550, 51.75744 24.29407... |
| 4 | ARG | Argentina | MULTIPOLYGON (((-65.50000 -55.20000, -66.45000... |
imports = pd.read_csv('data/imports.csv')
imports = imports.rename(columns={"imports": "Country"})
imports.head()
| Country | 1990 | 1991 | 1992 | 1993 | 1994 | 1995 | 1996 | 1997 | 1998 | ... | 2009 | 2010 | 2011 | 2012 | 2013 | 2014 | 2015 | 2016 | 2017 | 2018 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Afghanistan | 0.2499996 | 0.2499996 | 0.2499996 | 0.2499996 | 0.2499996 | 0.2499996 | 0.866684 | 2.899976 | 0.9999996 | ... | 6.0 | 10.0 | 1.0 | 5.0 | 3.0 | 8.0 | 12.0 | 11.0 | 34.0 | 39.0 |
| 1 | Africa | 2088.2558491999994 | 2875.796679499998 | 2089.2880337 | 2633.8870162000007 | 2717.1546953 | 1919.6889833 | 2306.0216044000003 | 2700.4105638 | 1748.7176231999997 | ... | 4437.0 | 4645.0 | 4486.0 | 4965.0 | 5810.0 | 5233.0 | 5530.0 | 5867.0 | 5240.0 | 6347.0 |
| 2 | Albania | 28.4169996 | 9.2829996 | 33.333 | 59.685 | 67.8020004 | 31.302 | 11.2749999 | 1.6509995 | 6.2479993 | ... | 122.1666575 | 113.652596 | 116.8118927 | 123.0 | 131.0 | 123.0 | 143.0 | 154.0 | 155.0 | 125.0 |
| 3 | Algeria | 1058.8870001 | 1782.264 | 862.2660006 | 1470.0250026 | 1697.6750046 | 897.7350043 | 1229.0680096 | 1461.0229982 | 475.3950068 | ... | 2066.0 | 2021.0 | 1942.0 | 2117.0 | 2125.0 | 2154.0 | 2159.0 | 2323.0 | 2190.0 | 2206.0 |
| 4 | Angola | Na | Na | Na | Na | Na | Na | Na | Na | Na | ... | Na | Na | Na | Na | Na | Na | Na | Na | Na | Na |
5 rows × 30 columns
imports = pd.melt(imports, id_vars = 'Country')
merged = world.merge(imports, on = 'Country')
merged = merged.rename(columns = {"variable": "Year", "value": "Import"})
merged['Year'] = merged['Year'].astype(int)
# merged.tail(5)
lst = []
for i in range(len(merged)):
r_num = merged['Import'][i]
if (r_num) != "Na":
round_num = round(float(r_num), 2)
lst.append(round_num)
merged['Import_rounded'] = lst
merged.head()
| id | Country | geometry | Year | Import | Import_rounded | |
|---|---|---|---|---|---|---|
| 0 | AFG | Afghanistan | POLYGON ((61.21082 35.65007, 62.23065 35.27066... | 1990 | 0.2499996 | 0.25 |
| 1 | AFG | Afghanistan | POLYGON ((61.21082 35.65007, 62.23065 35.27066... | 1991 | 0.2499996 | 0.25 |
| 2 | AFG | Afghanistan | POLYGON ((61.21082 35.65007, 62.23065 35.27066... | 1992 | 0.2499996 | 0.25 |
| 3 | AFG | Afghanistan | POLYGON ((61.21082 35.65007, 62.23065 35.27066... | 1993 | 0.2499996 | 0.25 |
| 4 | AFG | Afghanistan | POLYGON ((61.21082 35.65007, 62.23065 35.27066... | 1994 | 0.2499996 | 0.25 |
alt.data_transformers.disable_max_rows()
DataTransformerRegistry.enable('default')
input_slider = alt.binding_range(min = 1990, max = 2018, step=1, name = ' Select year: ')
select_year = alt.selection_single(name = "Year", fields = ['Year'], bind=input_slider, init = {'Year': 2018})
chart_1 = alt.Chart(merged
).project(type = 'equalEarth'
).mark_geoshape(
stroke = 'black', strokeWidth = 0.4
).encode(
color = alt.condition(
alt.datum['Import_rounded'] == 'Na',
alt.value('lightgrey'),
alt.Color('Import_rounded:Q', bin = alt.Bin(step = 4000),
legend = alt.Legend(title = "Imports, 60K-kg bags", orient = "bottom-left"),
scale = alt.Scale(scheme = 'goldgreen'))),
tooltip = [alt.Tooltip('Country:N'),
alt.Tooltip('Import_rounded:Q', title = 'Imports'),
alt.Tooltip('Year:Q')],
).properties(width = 700, height = 420, background = '#F9F9F9', padding = 25,
title = { "text": "In 2018 year, USA was the biggest coffee importer",
"subtitle": 'Worldwide coffee imports by years',
"subtitleFontSize": 14,
"subtitleFontWeight": 'bold'}
).configure_legend(titleFontSize=14, labelFontSize=13
).configure_axis(titleFontSize=14, labelFontSize = 12
).configure_title(fontSize=17
).add_selection(select_year
).transform_filter(select_year
)
chart_1.display(actions = False, renderer = 'png')
The purpose of the visualization was to show changes in prices paid to coffee growers in exporting countries in 2018 based on the 1990 year.
For the visualization, I considered the following ways: bar chart (simple to understand), encoding by color on map (cons: data is available only for 10 countries, so there is no need to show all world map), radar and bubble chart (cons: encoding by size is less convenient than by length).
I selected the horizontal bar chart. Each column corresponds to a country. The X-axis is not shown to not overload the visualization; instead, percentage values are displayed on bars, which are sorted in descending orders. Y-axis is still shown since, in my opinion, it is easier to interpret the chart. Tooltips also are added.
The main advantage of the chart is its easiness of understanding and comparing values.
prices = pd.read_csv('data/prices-paid-to-growers.csv')
prices = prices.rename(columns = {"prices_paid_to_growers": "Country"})
prices.head()
| Country | 1990 | 1991 | 1992 | 1993 | 1994 | 1995 | 1996 | 1997 | 1998 | ... | 2009 | 2010 | 2011 | 2012 | 2013 | 2014 | 2015 | 2016 | 2017 | 2018 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Colombia | 1.534724 | 1.481790 | 1.204656 | 1.106477 | 1.898327 | 2.199185 | 2.065245 | 2.939673 | 2.253433 | ... | 3.067442 | 3.985616 | 5.290868 | 3.679737 | 2.514539 | 3.525662 | 2.635854 | 2.727530 | 2.776185 | 2.505914 |
| 1 | Dominican Republic | 1.458168 | 1.382845 | 1.027841 | 1.172704 | 2.478234 | 2.412000 | 1.935342 | 3.673587 | 2.102616 | ... | 2.340731 | 3.071276 | 4.217812 | 3.262839 | 3.112084 | 4.052172 | 3.775011 | 3.911395 | 3.826974 | 3.391053 |
| 2 | El Salvador | 1.116194 | 0.983322 | 0.682322 | 0.780397 | 2.191177 | 2.218826 | 1.656073 | 2.762552 | 1.877384 | ... | 1.748205 | 2.425620 | 4.122256 | 2.651658 | 2.103276 | 2.582929 | 1.930439 | 1.976442 | 1.934159 | 1.645300 |
| 3 | Guatemala | 1.204956 | 1.270086 | 0.888099 | 0.914552 | 1.662711 | 2.192936 | 1.849170 | 2.146819 | 2.261521 | ... | 2.420375 | 3.195830 | 4.689066 | 3.664007 | 2.804848 | 3.373810 | 3.073230 | 2.990444 | 2.960790 | 2.697022 |
| 4 | Honduras | 1.111470 | 1.238947 | 0.886057 | 0.828746 | 1.800576 | 2.263781 | 1.493616 | 2.580166 | 2.058671 | ... | 1.848015 | 2.762887 | 4.415430 | 3.115841 | 2.247159 | 2.625614 | 2.449565 | 2.056691 | 2.003671 | 1.855291 |
5 rows × 30 columns
prices['Change'] = [ prices['2018'][i] - prices['1990'][i] for i in range(len(prices)) ]
prices['Change_%'] = [ prices['Change'][i] * 100 / prices['1990'][i] for i in range(len(prices))]
prices['Change_%_rounded'] = [ round(prices['Change_%'][i], 1) for i in range(len(prices)) ]
prices
| Country | 1990 | 1991 | 1992 | 1993 | 1994 | 1995 | 1996 | 1997 | 1998 | ... | 2012 | 2013 | 2014 | 2015 | 2016 | 2017 | 2018 | Change | Change_% | Change_%_rounded | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Colombia | 1.534724 | 1.481790 | 1.204656 | 1.106477 | 1.898327 | 2.199185 | 2.065245 | 2.939673 | 2.253433 | ... | 3.679737 | 2.514539 | 3.525662 | 2.635854 | 2.727530 | 2.776185 | 2.505914 | 0.971190 | 63.281072 | 63.3 |
| 1 | Dominican Republic | 1.458168 | 1.382845 | 1.027841 | 1.172704 | 2.478234 | 2.412000 | 1.935342 | 3.673587 | 2.102616 | ... | 3.262839 | 3.112084 | 4.052172 | 3.775011 | 3.911395 | 3.826974 | 3.391053 | 1.932885 | 132.555749 | 132.6 |
| 2 | El Salvador | 1.116194 | 0.983322 | 0.682322 | 0.780397 | 2.191177 | 2.218826 | 1.656073 | 2.762552 | 1.877384 | ... | 2.651658 | 2.103276 | 2.582929 | 1.930439 | 1.976442 | 1.934159 | 1.645300 | 0.529106 | 47.402677 | 47.4 |
| 3 | Guatemala | 1.204956 | 1.270086 | 0.888099 | 0.914552 | 1.662711 | 2.192936 | 1.849170 | 2.146819 | 2.261521 | ... | 3.664007 | 2.804848 | 3.373810 | 3.073230 | 2.990444 | 2.960790 | 2.697022 | 1.492066 | 123.827460 | 123.8 |
| 4 | Honduras | 1.111470 | 1.238947 | 0.886057 | 0.828746 | 1.800576 | 2.263781 | 1.493616 | 2.580166 | 2.058671 | ... | 3.115841 | 2.247159 | 2.625614 | 2.449565 | 2.056691 | 2.003671 | 1.855291 | 0.743821 | 66.922279 | 66.9 |
| 5 | India | 1.473558 | 1.358371 | 1.191159 | 1.278669 | 1.730810 | 1.870104 | 1.728881 | 2.873698 | 2.195885 | ... | 3.575634 | 2.584011 | 3.877464 | 3.467918 | 2.918461 | 2.996744 | 2.430896 | 0.957338 | 64.967746 | 65.0 |
| 6 | Uganda | 0.337598 | 0.654322 | 0.441397 | 0.552298 | 1.666651 | 1.506821 | 1.169927 | 1.772936 | 1.738302 | ... | 2.068040 | 1.545905 | 2.289245 | 1.794042 | 1.569419 | 1.699698 | 1.506691 | 1.169093 | 346.297047 | 346.3 |
| 7 | Brazil | 1.199223 | 0.971150 | 0.997768 | 1.167263 | 2.529110 | 2.550172 | 2.126834 | 3.209923 | 2.323380 | ... | 3.361764 | 2.254700 | 2.973442 | 2.306547 | 2.380366 | 2.438757 | 2.003980 | 0.804757 | 67.106552 | 67.1 |
| 8 | Ethiopia | 1.348565 | 1.505322 | 1.351128 | 1.362442 | 2.418234 | 2.539011 | 1.495287 | 2.010060 | 2.027901 | ... | 2.269088 | 1.618110 | 1.837430 | 1.678360 | 1.927316 | 1.799278 | 1.560234 | 0.211669 | 15.695859 | 15.7 |
| 9 | Togo | 0.645267 | 0.632307 | 0.658494 | 0.499857 | 0.573784 | 1.355618 | 1.381453 | 0.990868 | 1.102214 | ... | 1.512095 | 1.469380 | 1.321912 | 1.314625 | 1.451042 | 1.517221 | 1.565731 | 0.920464 | 142.648457 | 142.6 |
10 rows × 33 columns
bars = alt.Chart(prices).mark_bar().encode(
x = alt.X(field = 'Change_%', type = 'quantitative', title = 'Price change, %', axis = None),
y = alt.Y(field = 'Country', type = 'nominal', title = '',
sort = alt.Sort(field = 'Change_%', order = 'descending')),
tooltip = [alt.Tooltip('Country:N'),
alt.Tooltip('Change_%_rounded:Q', title = 'Change, %')],
color = alt.value('#87a725'))
text = bars.mark_text(
dx = -30 ,
align='left',
baseline='middle',
fontWeight= 'bold',
).encode(
text = alt.Text('Change_%_rounded:N'),
color = alt.value('white'))
chart_2 = bars + text
chart_2 = chart_2.properties(width = 700, height = 420, background = '#F9F9F9',
title = { "text": "Prices paid to growers in Uganda increased 3.5 times in 2018 compared to 1990",
"subtitle": 'Payment changes for coffee growers in 2018 based on 1990 year, in %',
"subtitleFontSize": 14,
"subtitleFontWeight": 'bold'}
).configure_view(strokeWidth = 0
).configure_legend(titleFontSize=14, labelFontSize=13
).configure_axis(titleFontSize=14, labelFontSize = 12
).configure_title(fontSize=18)
chart_2.display(actions = False, renderer = 'png')
The purpose of the visualization was to show payment changes for coffee growers in 2018 based on the 1990 year in USD/kg.
I considered the bar chart and line chart for the visualization, but I chose the dumbbell chart. Countries are located on the Y-axis, and prices for each country in 1990 and 2018 are located on the X-axis. Values for years vary by color. Countries are sorted in descending order by the largest prices. Also, tooltips with the country name, payments, and year are added.
The advantages of this visualization: convenient to compare values for countries with each other; easy to track changes; the ability to see specific data due to tooltips.
prices3 = prices[['Country', '1990', '2018']].copy()
prices3 = pd.melt(prices3, id_vars = 'Country')
prices3.columns = ['Country', 'Year', 'Price']
prices3['Price_rounded'] = [ round(prices3['Price'][i], 2) for i in range(len(prices3))]
prices3.head()
| Country | Year | Price | Price_rounded | |
|---|---|---|---|---|
| 0 | Colombia | 1990 | 1.534724 | 1.53 |
| 1 | Dominican Republic | 1990 | 1.458168 | 1.46 |
| 2 | El Salvador | 1990 | 1.116194 | 1.12 |
| 3 | Guatemala | 1990 | 1.204956 | 1.20 |
| 4 | Honduras | 1990 | 1.111470 | 1.11 |
lines = alt.Chart(prices3).mark_line().encode(
x = alt.X('max(Price_rounded):Q'),
x2 = alt.X2('min(Price_rounded):Q'),
y = alt.Y('Country:N',
sort = alt.Sort(field = 'Price_rounded', op = 'max', order='descending')))
points = alt.Chart(prices3).mark_circle(size = 150).encode(
x = alt.X('Price_rounded:Q', title = "Price change, USD/kg"),
y = alt.Y('Country:N', title = '',
sort = alt.Sort(field = 'Price_rounded', op = 'max', order = 'descending')),
color = alt.Color('Year:N', title = "Year"),
tooltip = [ alt.Tooltip('Country:N', title = 'Country'),
alt.Tooltip('Year:N', title = 'Year' ),
alt.Tooltip('Price_rounded:Q', title = 'Change' )])
chart_3 = alt.layer(lines, points
).properties(width = 500, height = 400, background = '#F9F9F9',
title = { "text": "Prices paid to coffee growers in Uganda were the highest in 2018 year",
"subtitle": 'Payment changes for coffee growers in 2018 based on 1990, in USD/kg',
"subtitleFontSize": 14,
"subtitleFontWeight": 'bold'}
).configure_view(strokeWidth = 0
).configure_legend(titleFontSize=14, labelFontSize=13
).configure_axis(titleFontSize=14, labelFontSize = 12
).configure_title(fontSize=18)
chart_3.display(actions = False, renderer = 'png')
The main visualization purpose is to display the ICO Composite and group indicator prices from 1990 to 2018.
For this visualization, I considered bar charts and adding interaction but selected a line chart because its main advantage is the easiness of interpretation.
Each line represents a type of coffee; the X-axis encodes years; Y-axis encodes prices indicators. There are tooltips with year, coffee type, and indicator price information.
ind_prices = pd.read_csv('data/indicator-prices.csv')
ind_prices.drop(['months' ], axis=1, inplace=True)
ind_prices = ind_prices.groupby(['Year']).mean().reset_index()
ind_prices = pd.melt(ind_prices, id_vars='Year')
ind_prices.columns = ['Year', 'Type', 'Ind prices']
ind_prices['Ind prices rounded'] = [ round(ind_prices['Ind prices'][i], 2) for i in range(len(ind_prices)) ]
ind_prices.tail()
| Year | Type | Ind prices | Ind prices rounded | |
|---|---|---|---|---|
| 140 | 2014 | Robustas | 2.217071 | 2.22 |
| 141 | 2015 | Robustas | 1.943709 | 1.94 |
| 142 | 2016 | Robustas | 1.955684 | 1.96 |
| 143 | 2017 | Robustas | 2.228440 | 2.23 |
| 144 | 2018 | Robustas | 1.871707 | 1.87 |
chart_4 = alt.Chart(ind_prices).mark_line(
).encode(
x = alt.X('Year:Q', title ='Year',
scale = alt.Scale(zero = False),
axis = alt.Axis(format = 'k', labelFontSize=12)),
y = alt.Y('Ind prices:Q', title = 'Indicator prices, USD/kg',
axis = alt.Axis(labelFontSize=12)),
detail = alt.Detail('Type:Q'),
tooltip = [alt.Tooltip('Type:N'),
alt.Tooltip('Ind prices rounded:Q', title = 'Ind. prices'),
alt.Tooltip('Year:Q')],
color = alt.Color('Type:N')
).properties(width = 700, height = 420, background = '#F9F9F9',
title = { "text": 'Robusta coffee has the lowest indicator prices',
"subtitle": 'Annual ICO Composite and group indicator prices, USD/kg',
"subtitleFontSize": 14,
"subtitleFontWeight": 'bold'}
).configure_view(strokeWidth = 0
).configure_legend(titleFontSize=14, labelFontSize=13
).configure_axis(titleFontSize=14, labelFontSize = 12
).configure_title(fontSize=18)
chart_4.display(actions = False, renderer = 'png')